package cn.yuanyuan.guide.lexer.domain;


import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import cn.yuanyuan.guide.lexer.entity.Token;

/**
 *
 * 字符串解析器
 *
 * @author wuyitao
 * @date 2022/3/26 11:56 PM
 * @updated 2022/3/27
 */
public class Tokenizer {


    /**
     * We're gonna start off with our first phase of parsing, lexical analysis, with
     * the tokenizer. <br/>
     * 这是解析的第一步，使用识别器进行词法分析。<br/>
     *
     * We're just going to take our string of code and break it down into an array
     * of tokens. <br/>
     * 我们需要获取代码字符串并把它分解成token数组。
     *
     *  (add 2 (subtract 4 2))   =>   [{ type: 'paren', value: '(' }, ...]
     *
     * @param input accepting an input string of code
     * @return 解析获取的tokens
     */
    public List<Token> tokenize(String input) {
        // A `current` variable for tracking our position in the code like a cursor.
        int current = 0;

        // And a `tokens` array for pushing our tokens to.
        List<Token> tokens = new ArrayList<>();

        // We start by creating a `while` loop where we are setting up our `current`
        // variable to be incremented as much as we want `inside` the loop.
        //
        // We do this because we may want to increment `current` many times within a
        // single loop because our tokens can be any length.
        while (current < input.length()) {

            // We're also going to store the `current` character in the `input`.
            char literal = input.charAt(current);

            // The first thing we want to check for is an open parenthesis. This will
            // later be used for `CallExpression` but for now we only care about the
            // character.
            //
            // We check to see if we have an open parenthesis:
            if (literal == '(') {

                // If we do, we push a new token with the type `paren` and set the value
                // to an open parenthesis.
                tokens.add(new Token(
                        "paren",
                        "("
                ));

                // Then we increment `current`
                current++;

                // And we `continue` onto the next cycle of the loop.
                continue;
            }

            // Next we're going to check for a closing parenthesis. We do the same exact
            // thing as before: Check for a closing parenthesis, add a new token,
            // increment `current`, and `continue`.
            if (literal == ')') {
                tokens.add(new Token(
                        "paren",
                        ")"
                ));
                current++;
                continue;
            }

            // Moving on, we're now going to check for whitespace. This is interesting
            // because we care that whitespace exists to separate characters, but it
            // isn't actually important for us to store as a token. We would only throw
            // it out later.
            //
            // So here we're just going to test for existence and if it does exist we're
            // going to just `continue` on.
            Pattern WHITESPACE = Pattern.compile("\\s");
            if (WHITESPACE.matcher(String.valueOf(literal)).find()) {
                current++;
                continue;
            }

            // The next type of token is a number. This is different than what we have
            // seen before because a number could be any number of characters and we
            // want to capture the entire sequence of characters as one token.
            //
            //   (add 123 456)
            //        ^^^ ^^^
            //        Only two separate tokens
            //
            // So we start this off when we encounter the first number in a sequence.
            Pattern NUMBERS = Pattern.compile("[0-9]");
            if (NUMBERS.matcher(String.valueOf(literal)).find()) {

                // We're going to create a `value` string that we are going to push
                // characters to.
                StringBuilder value = new StringBuilder();

                // Then we're going to loop through each character in the sequence until
                // we encounter a character that is not a number, pushing each character
                // that is a number to our `value` and incrementing `current` as we go.
                while (NUMBERS.matcher(String.valueOf(literal)).find()) {
                    value.append(literal);
                    literal = input.charAt(++current);
                }

                // After that we push our `number` token to the `tokens` array.
                tokens.add(new Token("number", value.toString()));

                // And we continue on.
                continue;
            }

            // We'll also add support for strings in our language which will be any
            // text surrounded by double quotes (").
            //
            //   (concat "foo" "bar")
            //            ^^^   ^^^ string tokens
            //
            // We'll start by checking for the opening quote:
            if (literal == '"') {
                // Keep a `value` variable for building up our string token.
                StringBuilder value = new StringBuilder();

                // We'll skip the opening double quote in our token.
                literal = input.charAt(++current);

                // Then we'll iterate through each character until we reach another
                // double quote.
                while (literal != '"') {
                    value.append(literal);
                    literal = input.charAt(++current);
                }

                // Skip the closing double quote.
                literal = input.charAt(++current);

                // And add our `string` token to the `tokens` array.
                tokens.add(new Token("string", value.toString()));
                continue;
            }

            // The last type of token will be a `name` token. This is a sequence of
            // letters instead of numbers, that are the names of functions in our lisp
            // syntax.
            //
            //   (add 2 4)
            //    ^^^
            //    Name token
            //
            Pattern LETTERS = Pattern.compile("[a-z]", Pattern.CASE_INSENSITIVE);
            if (LETTERS.matcher(String.valueOf(literal)).find()) {
                StringBuilder value = new StringBuilder();

                // Again we're just going to loop through all the letters pushing them to
                // a value.
                while (LETTERS.matcher(String.valueOf(literal)).find()) {
                    value.append(literal);
                    literal = input.charAt(++current);
                }

                // And pushing that value as a token with the type `name` and continuing.
                tokens.add(new Token("name", value.toString()));
                continue;
            }

            // Finally if we have not matched a character by now, we're going to throw
            // an error and completely exit.
            throw new RuntimeException("I dont know what this character is: " + literal);
        }

        // Then at the end of our `tokenizer` we simply return the tokens array.
        return tokens;
    }

}
